<?php
/**
 * In this file the class '\UF\Drawing\Size' is defined.
 *
 * @category   UniKap-Framework
 * @package    Drawing
 * @subpackage Core
 * @author     Ulf -UniKado- Kadner <ulfikado@gmail.com>
 * @since      2014-04-04 16:17
 * @version    0.1
 */

namespace UF\Drawing
{

    /**
     * This class defines a object for handling a size (width + height)
     *
     * @since  v0.1
     */
    class Size implements \UF\IStringConverter, \Serializable, \UF\IEquatable
    {

        # <editor-fold defaultstate="collapsed" desc="- - - -   P U B L I C   F I E L D S   - - - - - - - - - - - - - - - - - - - - - -">

        /**
         * The current width value.
         *
         * @var    int
         * @since  v0.1
         */
        public $Width;

        /**
         * The current height value.
         *
         * @var    int
         * @since  v0.1
         */
        public $Height;

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="- - - >   C O N S T R U C T O R   - - - - - - - - - - - - - - - - - - - - - - - -">

        /**
         * Inits a new instance.
         *
         * @param  int $width  The current width value. (Defaults to 0)
         * @param  int $height The current height value. (Defaults to 0)
         * @since  v0.1
         */
        public function __construct( $width = 0, $height = 0 )
        {
            $this->Width  = $width;
            $this->Height = $height;
            if ( 0 > $this->Width  ) $this->Width  = 0;
            if ( 0 > $this->Height ) $this->Height = 0;
        }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="- - - -   P U B L I C   M E T H O D S   - - - - - - - - - - - - - - - - - - - - -">

        # <editor-fold defaultstate="collapsed" desc="Checking Methods">

        /**
         * Returns if the current instance defines a empty size (width or height or both are 0)
         *
         * @return boolean
         * @since  v0.1
         */
        public function isEmpty()
        {
            return $this->Width <= 0 || $this->Height <= 0;
        }

        /**
         * Returns if the current size can contain the defined size.
         *
         * @param \UF\Drawing\Size $sz The other, maybe contained size
         * @return boolean
         * @since  v0.1
         */
        public final function contains( \UF\Drawing\Size $sz )
        {
            return (
                ( $sz->Width <= $this->Width ) &&
                ( $sz->Height <= $this->Height ) );
        }

        /**
         * Returns if the size defines a quadratic size (width must be equal to height)
         *
         * @return boolean
         * @since  v0.1
         */
        public final function isQuadratic()
        {
            return ( $this->Width == $this->Height );
        }

        /**
         * Returns if the height is bigger than the width. (Portrait format)
         *
         * @return boolean
         * @since  v0.1
         */
        public final function isPortrait()
        {
            return ( $this->Width < $this->Height );
        }

        /**
         * Returns if the width is bigger than the height. (Landscape format)
         *
         * @return boolean
         * @since  v0.1
         */
        public final function isLandscape()
        {
            return ( $this->Width > $this->Height );
        }

        /**
         * Returns if the size defines a nearly quadratic size. It means, that width and height can only have a
         * difference with a max. factor of 1.15. e.g. <code>width = 100, height = 115</code> defines a example for
         * max. factor 1.15 and portrait format. <code>width = 115, height = 100</code> defines a example for max.
         * factor 1.15 and landscape format.
         *
         * @return boolean
         * @since  v0.1
         */
        public final function isNearQuadratic()
        {
            if ( $this->isQuadratic() ) return true;
            if ( $this->isPortrait() )
                return 1.15 >= ( ( 0.0 + $this->Height ) / $this->Width );
            return 1.15 >= ( ( 0.0 + $this->Width ) / $this->Height );
        }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="Contracting Methods">

        /**
         * Minimizes both sides by defined value.
         *
         * @param  int $value
         * @since  v0.1
         */
        public final function contract( $value=1 )
        {
            $this->Width = $this->Width - $value;
            $this->Height = $this->Height - $value;
        }

        /**
         * Minimizes both sides by defined value and returns a new instance.
         *
         * @param  int $value
         * @return \UF\Drawing\Size
         * @since  v0.1
         */
        public final function contractClone( $value=1 )
        {
            $clone = clone $this;
            $clone->contract( $value );
            return $clone;
        }

        /**
         * Contracts the size by the defined percent value, while maintaining the proportions.
         *
         * @param  int $percent The conatracting percent value.
         * @since  v0.1
         */
        public final function contractProportional( $percent )
        {
            $this->Width = \intval( \floor( $this->Width * ( ( 100 - $percent ) / 100 ) ) );
            $this->Height = \intval( \floor( $this->Height * ( ( 100 - $percent ) / 100 ) ) );
        }

        /**
         * Contracts the size by defined percent value, while maintaining the proportions and returns a new instance.
         *
         * @param  int $percent The conatracting percent value.
         * @return \UF\Drawing\Size
         * @since  v0.1
         */
        public final function contractProportionalClone( $percent )
        {
            $clone = clone $this;
            $clone->contractProportional( $percent );
            return $clone;
        }

        /**
         * Reduces the current width and height, so that it fits a size specification while maintaining its proportions.
         *
         * @param  \UF\Drawing\Size $maxsize This size will be filled, but with maintaining the current proportions
         * @return boolean
         * @since  v0.1
         */
        public final function contractToMaxProportional( \UF\Drawing\Size $maxsize )
        {
            if ( ( $this->Width < $maxsize->Width ) && ( $this->Height < $maxsize->Height ) )   return false;
            if ( ( $this->Width == $maxsize->Width ) && ( $this->Height == $maxsize->Height ) ) return true;
            $dw = $this->Width / $maxsize->Width;
            #$pw = ( $dw < 1 ) ? 2.0 - $dw : $dw - 2.0;
            $dh = $this->Height / $maxsize->Height;
            #$ph = ( $dh < 1 ) ? 2.0 - $dh : $dh - 2.0;
            if ( $dw < $dh ) {
                $this->Width = \intval(\floor(($this->Width * $maxsize->Height) / $this->Height));
                $this->Height = $maxsize->Height; }
            elseif ( $dw > $dh ) {
                $this->Height = \intval(\floor(($maxsize->Width * $this->Height) / $this->Width));
                $this->Width = $maxsize->Width; }
            else {
                $this->Width = $maxsize->Width;
                $this->Height = $maxsize->Height; }
            return true;
        }

        /**
         * Reduces the current width and height, so that it fits a size specification while maintaining its proportions
         * and returns a new instance.
         *
         * @param  \UF\Drawing\Size $maxsize This size will be filled, but with maintaining the current proportions
         * @return \UF\Drawing\Size Or (boolean)FALSE
         * @since  v0.1
         */
        public final function contractToMaxProportionalClone( \UF\Drawing\Size $maxsize )
        {
            $clone = clone $this;
            if ( ! $clone->contractToMaxProportional( $maxsize ) ) return false;
            return $clone;
        }

        /**
         * Shrinks the size so that the longer side reduces as big as $ newMaxSideLength. For this, the smaller side
         * is reduced proportionally.
         *
         * @param  int $newMaxSideLength
         * @return boolean
         * @since  v0.1
         */
        public final function contractMaxSideTo( $newMaxSideLength )
        {
            if ( $this->isPortrait() )
            {
                if ( $newMaxSideLength >= $this->Height ) return false;
                $resultPercent = ( 100 * $newMaxSideLength ) / $this->Height;
                $this->Width = \intval( \floor( ( $resultPercent * $this->Width ) / 100 ) );
                $this->Height = $newMaxSideLength;
            }
            else
            {
                if ( $newMaxSideLength >= $this->Width ) return false;
                $resultPercent = ( 100 * $newMaxSideLength ) / $this->Width;
                $this->Height = \intval( \floor( ( $resultPercent * $this->Height ) / 100 ) );
                $this->Width = $newMaxSideLength;
            }
            return true;
        }

        /**
         * Shrinks the size so that the longer side reduces as big as $ newMaxSideLength. For this, the smaller side
         * is reduced proportionally and returns a new instance.
         *
         *
         * @param  int $newMaxSideLength
         * @return \UF\Drawing\Size Or (boolean)FALSE
         * @since  v0.1
         */
        public final function contractMaxSideToClone( $newMaxSideLength )
        {
            $clone = clone $this;
            if ( ! $clone->contractMaxSideTo( $newMaxSideLength ) ) return false;
            return $clone;
        }

        /**
         * Shrinks the size so that the longer side is reduced so it is exactly as large as $newLandscapeMaxWidth or
         * $newPortraitMaxHeight, depending on whether the size is in landscape or portrait format. For this,
         * the smaller side is reduced proportionally.
         *
         * @param  int $newLandscapeMaxWidth The max. width if size is landscape or quadratic
         * @param  int $newPortraitMaxHeight The max. height if size is portrait.
         * @return boolean
         * @since  v0.1
         */
        public final function contractMaxSideTo2( $newLandscapeMaxWidth, $newPortraitMaxHeight )
        {
            if ( $this->isPortrait() )
            {
                if ( $newPortraitMaxHeight >= $this->Height ) return false;
                $resultPercent = (100 * $newPortraitMaxHeight) / $this->Height;
                $this->Width = \intval( \floor( ( $resultPercent * $this->Width ) / 100 ) );
                $this->Height = $newPortraitMaxHeight;
            }
            else
            {
                if ( $newLandscapeMaxWidth >= $this->Width ) return false;
                $resultPercent = ( 100 * $newLandscapeMaxWidth ) / $this->Width;
                $this->Height = \intval( \floor( ( $resultPercent * $this->Height ) / 100 ) );
                $this->Width = $newLandscapeMaxWidth;
            }
            return true;
        }

        /**
         * Shrinks the size so that the longer side is reduced so it is exactly as large as $newLandscapeMaxWidth or
         * $newPortraitMaxHeight, depending on whether the size is in landscape or portrait format. For this,
         * the smaller side is reduced proportionally and returns a new instance.
         *
         * @param  int $newLandscapeMaxWidth The max. width if size is landscape or quadratic
         * @param  int $newPortraitMaxHeight The max. height if size is portrait.
         * @return \UF\Drawing\Size Or (boolean)FALSE
         * @since  v0.1
         */
        public final function contractMaxSideTo2Clone( $newLandscapeMaxWidth, $newPortraitMaxHeight )
        {
            $clone = clone $this;
            if ( ! $clone->contractMaxSideTo2( $newLandscapeMaxWidth, $newPortraitMaxHeight ) ) return false;
            return $clone;
        }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="XML-Methods">

        /**
         * Returns a string that contains all instance data as valid XML attributes. (e.g.:
         * <code>width="100" height="160"</code>)
         *
         * @return string
         * @since  v0.1
         */
        public final function toXMLParams()
        {
            return \sprintf( 'width="%d" height="%d"', $this->Width, $this->Height );
        }

        /**
         * Writes the 2 XML attributes width and height to defined XmlWriter. So current a XML element must be started
         * an not closed by writer!
         *
         * @param \XMLWriter $w
         * @since  v0.1
         */
        public final function writeXMLAttributes( \XMLWriter $w )
        {
            $w->writeAttribute( 'width',  $this->Width  );
            $w->writeAttribute( 'height', $this->Height );
        }

        /**
         * Writes a XML element with the defined name and the attributes 'width' and 'height'.
         *
         * @param \XMLWriter $w
         * @param  string    $elementName The XML element name. (Defaults to 'Size')
         * @since  v0.1
         */
        public final function writeXML( \XMLWriter $w, $elementName = 'Size' )
        {
            $w->startElement( empty( $elementName ) ? 'Size' : $elementName );
            $this->writeXmlAttributes( $w );
            $w->endElement();
        }

        /**
         * Reads the data from the defined XML element (attributes 'width' + 'height' required) and on success
         * the current instance data will be overwritten by new data.
         *
         * @param \SimpleXMLElement $xmlElement
         * @return boolean
         * @since  v0.1
         */
        public final function readXML( \SimpleXMLElement $xmlElement )
        {
            if ( !isset( $xmlElement['width'] ) || !isset( $xmlElement['height'] ) ) return false;
            $w = \intval( (string) $xmlElement['width'] );
            $h = \intval( (string) $xmlElement['height'] );
            if ( 0 > $w || 0 > $h ) return false;
            $this->Width = $w;
            $this->Height = $h;
            return true;
        }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="Rotation Methods">

        /**
         * Rotates the current size by 90°
         *
         * @since  v0.1
         */
        public final function rotateSquare()
        {
            $tmp = $this->Width;
            $this->Width = $this->Height;
            $this->Height = $tmp;
        }

        /**
         * Rotates the current size by 90° and returns a new instance.
         *
         * @return \UF\Drawing\Size
         * @since  v0.1
         */
        public final function rotateSquareClone()
        {
            $tmp = clone $this;
            $tmp->rotateSquare();
            return $tmp;
        }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="\UF\IStringConverter">

        /**
         * Returns the instance data string with format: <code>width=?; height=?</code>
         *
         * @return string
         * @since  v0.1
         */
        public function __toString()
        {
            return $this->toString();
        }

        /**
         * Returns the instance data string with format: <code>width=?; height=?</code>
         *
         * @return string
         * @since  v0.1
         */
        public function toString()
        {
            return \sprintf( 'width=%d; height=%d', $this->Width, $this->Height );
        }

        /**
         * Reads the data from the defined array (keys 'width' + 'height' required) and on success
         * the current instance data will be overwritten by new data.
         *
         * @param array $objectData Array with the keys 'width' and 'height'.
         * @since  v0.1
         */
        public function fromArray( array $objectData )
        {
            if ( isset( $objectData['width'] ) )      $this->Width = \inval( $objectData['width'] );
            elseif ( isset( $objectData['Width'] ) )  $this->Width = \inval( $objectData['Width'] );
            elseif ( isset( $objectData[0] ) )        $this->Width = \inval( $objectData[0] );
            if ( isset( $objectData['height'] ) )     $this->Height = \inval( $objectData['height'] );
            elseif ( isset( $objectData['Height'] ) ) $this->Height = \inval( $objectData['Height'] );
            elseif ( isset( $objectData[1] ) )        $this->Height = \inval( $objectData[1] );
        }

        /**
         * Reads the data from the defined strng (for usable format see {@see \UF\Drawing\Size\toString()}) and
         * on success the current instance data will be overwritten by new data.
         *
         * @param  string $objectString
         * @return boolean
         * @since  v0.1
         */
        public function fromString( $objectString )
        {
            $hits = null;
            if ( !\preg_match( '~^width=(\d{1,4});\s*height=(\d{1,4})$~', $objectString, $hits ) )
            {
                if ( !\preg_match( '~^height=(\d{1,4});\s*width=(\d{1,4})$~', $objectString, $hits ) )
                    return false;
                $this->Width  = \intval( $hits[2] );
                $this->Height = \intval( $hits[1] );
            }
            else
            {
                $this->Width  = \intval( $hits[1] );
                $this->Height = \intval( $hits[2] );
            }
            return true;
        }

        /**
         * Returns the width and the height as a associative array with the keys
         * <code>0 = width, 1 = height, 'width' and 'height'</code>
         *
         * @return array Array with Keys <code>0 = width, 1 = height, 'width' and 'height'</code>
         * @since  v0.1
         */
        public function toArray()
        {
            return [
                0        => $this->Width,
                1        => $this->Height,
                'width'  => $this->Width,
                'height' => $this->Height
            ];
        }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="Implement Serializable">

        /**
         * Returns the instance data, serialized to a string.
         *
         * @return string
         * @since  v0.1
         */
        public function serialize()
        {
            return \serialize( $this->toArray() );
        }

        /**
         * Constructs the object from serialized data.
         *
	 * @param  string $serialized The serialized string representation of the object.
         * @since  v0.1
         */
        public function unserialize( $serialized )
        {
            $this->fromArray( \unserialize( $serialized ) );
        }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="Implement \UF\IEquatable">

        /**
         * Checks if the current instance is equal to defined value.
         *
         * If the 2nd parameter is TRUE, $value can only be equal if its a instance of {@see \UF\Drawing\Size}.
         * Otherwise also values are checked, that can be converted to a {@see \UF\Drawing\Size}, with
         * {@see \UF\Drawing\Size::TryParse()}.
         *
         * @param  \UF\Drawing\Size|int|array|string|image-resource $value
         * @param  boolean $strict Allow only values of type {@see \UF\Drawing\Size}? (Defaults to FALSE)
         * @return boolean
         * @since  v0.1
         */
        public function equals( $value, $strict=false )
        {
            if ( $value instanceof \UF\Drawing\Size )
                return $value->Width  == $this->Width && $value->Height == $this->Height;
            if ( $strict ) return false;
            $size = null;
            if ( !self::TryParse( $value, $size ) ) return false;
            return $size->Width  == $this->Width && $size->Height == $this->Height;
        }
        
        # </editor-fold>

        /**
         * Makes a Size cloneable.
         *
         * @return \UF\Drawing\Size
         * @since  v0.1
         */
        public function __clone()
        {
            return new \UF\Drawing\Size( $this->Width, $this->Height );
        }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="- - - -   P U B L I C   S T A T I C   M E T H O D S   - - - - - - - - - - - - - -">

        /**
         * Extracts from the passed value a {@ see \UF\Drawing\Size} instance, an returns it with the $size parameter
         * if the method self returns TRUE.
         *
         * @param  mixed $value int|double|array|string|\SimpleXMLElement|image-resource
         * @param  \UF\Drawing\Size $size Resulting {@see \UF\Drawing\Size} instance if method returns TRUE.
         * @return boolean
         * @since  v0.1
         */
        public static function TryParse( $value, &$size )
        {
            $size = null;
            if ( \is_int( $value ) ) {
                $size = new \UF\Drawing\Size($value, $value); return true; }
            if ( \is_double( $value ) ) {
                $size = new \UF\Drawing\Size( (int)$value, (int)$value ); return true; }
            if ( \is_array( $value ) )
            {
                if ( \count( $value ) < 2 ) return false;
                if ( \UF\ArrayHelper::IsNumericIndexed( $value ) ) {
                    $size = new \UF\Drawing\Size( \intval($value[0]), \intval($value[1]) );
                    return true; }
                $w = 0; $h = 0;
                if ( isset( $value['w'] ) )          $w = \intval($value['w']);
                elseif ( isset( $value['width'] ) )  $w = \intval($value['width']);
                elseif ( isset( $value['Width'] ) )  $w = \intval($value['Width']);
                else                                 return false;
                if ( isset( $value['h'] ) )          $h = \intval($value['h']);
                elseif ( isset( $value['height'] ) ) $h = \intval($value['height']);
                elseif ( isset( $value['Height'] ) ) $h = \intval($value['Height']);
                else                                 return false;
                $size = new \UF\Drawing\Size( $w, $h );
                return true;
            }
            if ( \is_string( $value ) )
            {
                $hits = null;
                if ( \preg_match( '~^width=(\d{1,4});\s*height=(\d{1,4})$~', $value, $hits ) ) {
                    $size = new \UF\Drawing\Size( \intval( $hits[1] ), \intval( $hits[2] ) );
                    return true; }
                if ( \preg_match( '~^height=(\d{1,4});\s*width=(\d{1,4})$~', $value, $hits ) ) {
                    $size = new \UF\Drawing\Size( \intval( $hits[2] ), \intval( $hits[1] ) );
                    return true; }
                $res = \UF\ArrayHelper::ParseAttributes( $value );
                if ( empty($res) || !isset($res['width']) || !isset($res['height']) ) {
                    if (!\preg_match('~^[A-Za-z0-9_.:,;/!$%~*+-]+$~', $value) ) return false;
                    if ( !\file_exists($value) )                                return false;
                    try { $tmp = \getimagesize( $value ); }
                    catch ( \Exception $ex ) { return false; }
                    $size = new \UF\Drawing\Size( \intval( $tmp[0] ), \intval( $tmp[1] ) );
                    return true; }
                $w = \strval( \intval( $res['width'] ) );
                $h = \strval( \intval( $res['height'] ) );
                if ($h != $res['height'] || $w < $res['width']) return false;
                $size = new \UF\Drawing\Size( \intval( $res['width'] ), \intval( $res['height'] ) );
                return true;
            }
            if ( $value instanceof \SimpleXMLElement )
            {
                if ( !isset( $value['width'] ) || !isset( $value['height'] ) ) return false;
                $size = new \UF\Drawing\Size( \intval( (string)$value['width'] ), \intval( (string)$value['height'] ) );
                return true;
            }
            if ( \is_resource( $value ) && \UF\str_contains(\get_resource_type($value), 'image', true) )
            {
                try {
                    $size = new \UF\Drawing\Size( \imagesx( $value ), \imagesy( $value ) );
                    return true; }
                catch ( \Exception $ex ) { return false; }
            }
            return false;
        }

        /**
         * Parses a string with format <code>width=?; height=?</code> to a new {@see \UF\Drawing\Size} instance.
         *
         * @param  string $str
         * @return \UF\Drawing\Size Or boolean FALSE
         * @since  v0.1
         */
        public static function Parse( $str )
        {
            $res = new \UF\Drawing\Size();
            if ( ! $res->fromString( $str ) ) return false;
            if ( $res->isEmpty() )            return false;
            return $res;
        }

        /**
         * Parse a string that contains some valid XML element attributes to a new {@see \UF\Drawing\Size} instance.
         *
         * @param  string $paramsStr XML-Attributes (e.g.: 'width="100" height="50"')
         * @return \UF\Drawing\Size Or boolean FALSE
         * @since  v0.1
         */
        public static function ParseXmlParams( $paramsStr )
        {
            $res = \UF\ArrayHelper::ParseAttributes( $paramsStr );
            if ( empty( $res ) || ! isset( $res['width'] ) || ! isset( $res['height'] ) )
                return false;
            $w = \strval( \intval( $res['width']  ) );
            $h = \strval( \intval( $res['height'] ) );
            if ( $h != $res['height'] || $w < $res['width'] )
                return false;
            return new \UF\Drawing\Size( \intval( $res['width'] ), \intval( $res['height'] ) );
        }

        /**
         * Parse a XML element to a new {@see \UF\Drawing\Size} instance.
         *
         * @param  \SimpleXMLElement $xml
         * @return \UF\Drawing\Size Or boolean FALSE
         * @since  v0.1
         */
        public static function ParseXml( \SimpleXMLElement $xml )
        {
            if ( ! isset( $xml['width'] ) || ! isset( $xml['height'] ) )
                return false;
            return new \UF\Drawing\Size( \intval( (string)$xml['width'] ), \intval( (string)$xml['height'] ) );
        }

        /**
         * @return \UF\Drawing\Size
         * @throws \UF\IO\FileNotFoundException
         * @throws \UF\IO\Exception
         * @since  v0.1
         */
        public static function FromImageFile( $imageFile )
        {
            if ( !\file_exists( $imageFile ) ) throw new \UF\IO\FileNotFoundException( $imageFile );
            try {
                $tmp = \getimagesize( $imageFile );
                return new \UF\Drawing\Size( \intval( $tmp[0] ), \intval( $tmp[1] ) );
            }
            catch ( \Exception $ex ) { throw new \UF\IO\Exception( $imageFile, $ex->getMessage() ); }
        }

        # </editor-fold>

    }

}

